package com.osce.common.utils.oesc;

import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.doudou.core.container.ListUtil;
import com.doudou.core.container.MapUtil;
import com.doudou.core.container.SetUtil;
import com.doudou.core.lang.ObjUtil;
import com.doudou.core.random.RandomUtil;

import java.util.*;

/**
 * @author zzxjs
 * @since 2023/9/8
 * osce分配考站的请求对象，包含所需要的请求参数
 * 并且执行排站能力
 */
public class OsceAllotRequest {

    private static final Long ONE_MS = 1000 * 60L;//一秒的毫秒值

    /** 开始日期 */
    private Date startDate;
    /** 结束日期 */
    private Date endDate;
    /** 考站集合 */
    private final List<PlaceModel> placeList = ListUtil.linkedList();
    /** 时间分段集合 */
    private final List<TimeModel> timeList = ListUtil.linkedList();
    /** 人数 */
    private Integer noses;
    /** 考试间隔时间(分) */
    private Integer interval;
    /** 如果时间冲突，需要分配给考生前往下一个考站的时间(分) 默认两分钟*/
    private Integer collideTime = 2;
    /** 是否是人员优先 true人员优先、false资源优先 */
    private Boolean personnelPriority;
    /** 所有学员模型 */
    Map<String, PeopleStatus> peoploStatusMap;
    /** 已考完学员容器 */
    Map<Long, Set<String>> examPeopleMap;


    /** 无参构造 */
    public OsceAllotRequest() {}
    /** 有参构造 */
    public OsceAllotRequest(Date startDate, Date endDate, Integer noses, Integer interval, Boolean personnelPriority) {
        this.startDate = startDate;
        this.endDate = endDate;
        this.noses = noses;
        this.interval = interval;
        this.personnelPriority = personnelPriority;
    }


    public List<PlaceModel> getPlaceList() {
        return placeList;
    }

    public List<TimeModel> getTimeList() {
        return timeList;
    }

    /**
     * 添加房间模型
     */
    public OsceAllotRequest addPlace(PlaceModel...placeModels){
        if (ObjUtil.isNotEmpty(placeModels)){
            this.placeList.addAll(Arrays.asList(placeModels));
        }
        return this;
    }

    /**
     * 添加时间分段模型
     */
    public OsceAllotRequest addTime(TimeModel...timeModels){
        if (ObjUtil.isNotEmpty(timeModels)){
            this.timeList.addAll(Arrays.asList(timeModels));
        }
        return this;
    }

    /**
     * 排站方法
     */
    public OsceAllotResponse placeAllot(){
        OsceAllotResponse response = new OsceAllotResponse(this);

        this.parsePeopleStatus();//创建出所有学员模型
        this.parseExamPeople();//创建已考完的考生的容器

        /* 1、计算出考试的天数，循环这个天数
         * 2、在每一天循环时间分段
         * 3、在每一个时间分段循环每一分钟来模拟考场现场安排学员 */
        long examDay = DateUtil.betweenDay(this.startDate, this.endDate, false) + 1;//计算考试天数
        for (long day = 0; day < examDay; day++) {//循环天数
            String nowDateStr = DateUtil.offsetDay(this.startDate,(int)day).toDateStr();//当前的日期字符串
            for (TimeModel timeModel : this.timeList) {//循环时间分段
                /* 对该时间分段开始分配 */
                placeAllot(nowDateStr,timeModel.getStart(), timeModel.getEnd(),response);
            }
        }
        return response;
    }

    /**
     * 核心排站方法
     */
    private void placeAllot(String nowDateStr,Date startTime,Date endTime,OsceAllotResponse response){
        LinkedHashMap<Long, LinkedList<PlaceModel>> placeModelMap = this.createPlaceModel();//创建考站模型
        int interval_M = this.interval - 1;
        //模拟时间每一分钟的过去
        DateTime nowTime = DateTime.of(startTime);
        int minPeopleNum = this.personnelPriority ? calculatePlaceModelMin(endTime.getTime() - startTime.getTime()) : 0;
        for (int i = 0; i <= DateUtil.between(startTime,endTime,DateUnit.MINUTE); i++) {
            //循环房间，插入考生
            for (Map.Entry<Long, LinkedList<PlaceModel>> entry : placeModelMap.entrySet()) {
                Long placeId = entry.getKey();
                List<PlaceModel> models = entry.getValue();
                for (PlaceModel model : models) {
                    if (this.personnelPriority && model.getPlaceAllotList().size() >= minPeopleNum){//人员优先，限制时间段人数【木桶原则：一个木桶的容量取决于最短的那块木板】
                        break;
                    }
                    if (!model.getNasPeoPle()) {//无人，可插入
                        String code = this.firstWaitPeople(placeId);//查询第一个没有在该考场考过的等待的人员
                        DateTime endTimeFlag = new DateTime(nowTime).offset(DateField.MINUTE, model.getExTime());//计算结束时间
                        if (code != null && DateUtil.isIn(endTimeFlag, startTime, endTime)) {
                            PeopleStatus peopleStatus = this.peoploStatusMap.get(code);
                            if (peopleStatus.containsTime(nowTime)) {//如果安排过的考试包含当前时间，则需要给考生设定的前往下一个考站的时间
                                endTimeFlag = endTimeFlag.offset(DateField.MINUTE, this.collideTime);//结束时间在加上设定时间
                                //将考生插入考场
                                model.addPlaceAllot(new PlaceAllotBean(code, nowTime.offset(DateField.MINUTE, this.collideTime), endTimeFlag));
                                model.setCode(code);
                                setExamMap(placeId, code);//记录考站考过的考生
                                this.peoploStatusMap.put(code, peopleStatus.addTimeSet(endTimeFlag).wait(true));//修改学生的等待状态
                                //修改考场状态
                                model.setNasPeoPle(true);//有人
                                model.parseWaitMinute(interval_M);//格式化等待时间=考试时间+等待时间
                                model.setPeopleExamMinute(model.getExTime());
                                model.addWaitMinute(this.collideTime);//等待时间要再加上考生设定的前往下一个考站的时间
                            } else {
                                //将考生插入考场
                                model.addPlaceAllot(new PlaceAllotBean(code, nowTime, endTimeFlag));
                                model.setCode(code);
                                setExamMap(placeId, code);//记录考站考过的考生
                                this.peoploStatusMap.put(code, peopleStatus.addTimeSet(endTimeFlag).wait(true));//修改学生的等待状态
                                //修改考场状态
                                model.setNasPeoPle(true);//有人
                                model.parseWaitMinute(interval_M);//格式化等待时间=考试时间+等待时间
                                model.setPeopleExamMinute(model.getExTime());
                            }
                        }
                    } else {//有人，但是过去一分钟，等待时间减一
                        String codeFlag;
                        if (StrUtil.isNotBlank(codeFlag = model.minusWaitMinute(1))) {
                            this.peoploStatusMap.put(codeFlag, this.peoploStatusMap.get(codeFlag).wait(false));//修改学生的等待状态
                        }
                    }
                }
            }
            nowTime = DateUtil.offset(nowTime, DateField.MINUTE,1);
        }
        response.addPlaceAllot(nowDateStr,placeModelMap);
    }

    /**
     * 创建考站模型
     * { "1": [{"id": 1, "intervalTime": 3}, {"id": 1, "intervalTime": 3}],
     *    "2": [{"id": 2, "intervalTime": 4}] }
     */
    private LinkedHashMap<Long, LinkedList<PlaceModel>> createPlaceModel(){
        LinkedHashMap<Long, LinkedList<PlaceModel>> placeModelMap = new LinkedHashMap<>();
        for (PlaceModel model : this.placeList) {
            LinkedList<PlaceModel> places = new LinkedList<>();
            for (int i = 0; i < model.getPlaceCount(); i++) {
                places.add(new PlaceModel(model.getId(),model.getExTime()));
            }
            placeModelMap.put(model.getId(),places);
        }
        return placeModelMap;
    }

    /**
     * 创建考生状态模型
     */
    private void parsePeopleStatus(){
        String codePrefix = RandomUtil.singleLetterUpper();//随机出一个大写字母做编号前缀
        this.peoploStatusMap = MapUtil.map(true);
        for (int i = 1; i <= this.noses; i++) {
            MapUtil.put(this.peoploStatusMap,codePrefix+i,new PeopleStatus());
        }
    }

    /**
     * 创建已经考完考生容器
     */
    private void parseExamPeople(){
        this.examPeopleMap = MapUtil.map(true);
        for (PlaceModel model : this.placeList) {
            MapUtil.put(this.examPeopleMap,model.getId(), SetUtil.hashSet());
        }
    }

    /**
     * 记录考生已经考过该考站
     * @param placeId 考站编号
     * @param code 考生代号
     */
    private void setExamMap(Long placeId,String code){
        Set<String> codes = this.examPeopleMap.get(placeId);
        codes.add(code);
        this.examPeopleMap.put(placeId,codes);
    }

    /**
     * 查询学员map里第一个等待的人员
     * 并且没有考过该考场
     * 都没有空闲就返回null
     */
    private String firstWaitPeople(Long placeId) {
        for (Map.Entry<String, PeopleStatus> entry : this.peoploStatusMap.entrySet()) {
            if (!entry.getValue().getWait() && !this.examPeopleMap.get(placeId).contains(entry.getKey())) return entry.getKey();
        }
        return null;
    }

    /**
     * 计算出考场最多支持人数的那一个-资源优先
     * @param examDuration 考试总时长 start-end 10800000
     * @param interval_M 每场考试的间隔时间(休息时间) 10(分钟)
     * @param placeList 考场集合
     * @return { "1": {"id": 1, "intervalTime": 3, "capacity": 14}}
     */
    private int calculatePlaceModelMax(long examDuration,int interval_M,List<PlaceModel> placeList){
        // 计算每场考试的时间段
        long examInterval = interval_M * ONE_MS;//间隔时间
        //先找出考试时间最少的那个，那一个肯定支持的人数最多
        int minExTime = placeList.stream().min(Comparator.comparingInt(PlaceModel::getExTime)).get().getExTime();
        long placeExamDuration = minExTime * ONE_MS;//一场考试要用的时间
        long initDuration = 0;//已用时间，初始值为0
        int peopleCount = 0;//已考人数，初始值为0
        while ((initDuration+=placeExamDuration) <= examDuration){//要求已考时间不能大于考试总时间
            ++peopleCount;//如果时间没到，人数加1
            initDuration+=examInterval;//最后加上间隔时间
        }
        return peopleCount;
    }

    /**
     * 计算出考场最少支持人数的那一个-人员优先
     * @param examDuration 考试总时长 start-end 10800000
     * @return { "1": {"id": 1, "intervalTime": 3, "capacity": 14}}
     */
    public int calculatePlaceModelMin(long examDuration){
        // 计算每场考试的时间段
        long examInterval = this.interval * ONE_MS;//间隔时间
        //先找出考试时间最多的那个，那一个肯定支持的人数最少
        int maxExTime = this.placeList.stream().max(Comparator.comparingInt(PlaceModel::getExTime)).get().getExTime();
        long placeExamDuration = maxExTime * ONE_MS;//一场考试要用的时间
        long initDuration = 0;//已用时间，初始值为0
        int peopleCount = 0;//已考人数，初始值为0
        while ((initDuration+=placeExamDuration) <= examDuration){//要求已考时间不能大于考试总时间
            ++peopleCount;//如果时间没到，人数加1
            initDuration+=examInterval;//最后加上间隔时间
        }
        return peopleCount;
    }
}
